/**
 * The MIT License
 * Copyright (c) 2010 Tad Glines
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package net.towee.server.sio.server;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

import javax.servlet.GenericServlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.towee.server.sio.server.transport.FlashSocketTransport;
import net.towee.server.sio.server.transport.HTMLFileTransport;
import net.towee.server.sio.server.transport.JSONPPollingTransport;
import net.towee.server.sio.server.transport.WebSocketTransport;
import net.towee.server.sio.server.transport.XHRPollingTransport;

import org.eclipse.jetty.util.IO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public abstract class SocketIOServlet extends GenericServlet {
	private static final long serialVersionUID = -8365399673642347743L;
	private static final Logger LOG = LoggerFactory.getLogger(SocketIOServlet.class);
	
	// Configuration of our Socket.IO server
	public static final int BUFFER_SIZE_DEFAULT = 8192;
	public static final int MAX_IDLE_TIME_DEFAULT = 300*1000;
	public static final int HEARTBEAT_INTERVAL = 15;
	public static final int HEARTBEAT_TIMEOUT = 30;
	public static final int CLOSE_TIMEOUT = 60;
	
	// Version of implemented protocol
	public static final int PROTOCOL_VERSION = 1;
	
	// Supported transports list
	private static final Map<String, Transport> transports;
	private static final String transportsForHandshake;
	
	// Create session manager
	private SocketIOSessionManager sessionManager = new SocketIOSessionManager();
	
	// Initialize static information about transports
	static {
		// Create map with transports
		transports = new TreeMap<String, Transport>();
		
		// Create objects
		WebSocketTransport websocketTransport = new WebSocketTransport(BUFFER_SIZE_DEFAULT, MAX_IDLE_TIME_DEFAULT );
		FlashSocketTransport flashsocketTransport = new FlashSocketTransport(BUFFER_SIZE_DEFAULT, MAX_IDLE_TIME_DEFAULT );
		//HTMLFileTransport htmlFileTransport = new HTMLFileTransport(BUFFER_SIZE_DEFAULT, MAX_IDLE_TIME_DEFAULT );
		XHRPollingTransport xhrPollingTransport = new XHRPollingTransport(BUFFER_SIZE_DEFAULT, MAX_IDLE_TIME_DEFAULT );
		//JSONPPollingTransport jsonpPollingTransport = new JSONPPollingTransport(BUFFER_SIZE_DEFAULT, MAX_IDLE_TIME_DEFAULT );
		
		// Put to map
		transports.put(xhrPollingTransport.getName(), xhrPollingTransport);
		transports.put(websocketTransport.getName(), websocketTransport);
		//transports.put(flashsocketTransport.getName(), flashsocketTransport);
		//transports.put(htmlFileTransport.getName(), htmlFileTransport);
		//transports.put(jsonpPollingTransport.getName(), jsonpPollingTransport);
		
		// Create string for handshaking and init all transports
		StringBuilder strBuilder = new StringBuilder();
		for (Transport t : transports.values()) {
			strBuilder.append( t.getName() );
			strBuilder.append( "," );
			t.init();
		}
		strBuilder.deleteCharAt( strBuilder.length()-1 );
		transportsForHandshake = strBuilder.toString();
	}

	@Override
	public void service(ServletRequest request, ServletResponse response)
			throws ServletException, IOException {
		service((HttpServletRequest)request, (HttpServletResponse)response);
	}

    protected void service(HttpServletRequest request, HttpServletResponse response)
    		throws ServletException, IOException {
    	if ("OPTIONS".equals(request.getMethod()))
    		return;

    	// Check contains of version
    	String path = request.getPathInfo();
    	if (path == null || path.length() == 0 || "/".equals(path)) {
    		response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing SocketIO version");
    		return;
    	}
    	
    	if (path.startsWith("/")) path = path.substring(1);
    	
    	// Split path to parts
    	String[] parts = path.split("/");
    	
    	// First piece is protocol version, check it
    	if("GET".equals(request.getMethod())) {
    		if("socket.io.js".equals(parts[0])) {
    			LOG.info("Send socket.io JS");
    			sendSocketIOScript(request, response);
    			return;
    		}
    	}
    	else if(!parts[0].equals(PROTOCOL_VERSION+"")) {
    		response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Wrong SocketIO protocol version: "+parts[0]);
    		return;    		
    	}
    	
    	// If parts consists of only one piece -> it's handshake
    	if( parts.length == 1 ) 
    		processHandshake(parts, request, response);
    	else
    		processRequest(parts, request, response);
    }
    
    /**
     * Send the socket.io JS
     * @param request
     * @param response
     * @throws IOException
     */
    private void sendSocketIOScript(HttpServletRequest request, HttpServletResponse response) throws IOException {
		response.setContentType("text/javascript;charset=utf-8");
		InputStream is = this.getClass().getClassLoader().getResourceAsStream("net/towee/server/sio/socket.io.js");
		OutputStream os = response.getOutputStream();
		IO.copy(is, os);		
	}

	/**
     * Process the handshake request
     * @param parts
     * @param request
     * @param response
     * @throws IOException 
     */
	private void processHandshake(String[] parts, HttpServletRequest request, HttpServletResponse response) throws IOException {
		LOG.info("Process Socket.IO Handshake");
		
		// Create sesssion
		SocketIOSession sess = sessionManager.createSession();
		response.setContentType("text/plain;charset=utf-8");
		
		// Send handshake
		OutputStreamWriter writer = new OutputStreamWriter(response.getOutputStream());
		writer.append( sess.getSessionId() );
		writer.append( ":" );
		writer.append( HEARTBEAT_TIMEOUT+"" );
		writer.append( ":" );
		writer.append( CLOSE_TIMEOUT+"" );
		writer.append( ":" );
		writer.append( transportsForHandshake );
		writer.flush();
	}

	/**
	 * Process transport request
	 * @param parts
	 * @param request
	 * @param response
	 * @throws IOException
	 */
    private void processRequest(String[] parts, HttpServletRequest request, HttpServletResponse response) throws IOException {
    	Transport transport = transports.get( parts[1] );
    	
    	if (transport == null) {
	    	response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unknown SocketIO transport");
	    	return;
    	}
    	
    	LOG.info("Process Socket.IO transport request: "+parts[1]);
  		transport.handle(request, response, new Transport.InboundFactory() {
			@Override
			public SocketIOInbound getInbound(HttpServletRequest request, String[] protocols) {
				return SocketIOServlet.this.doSocketIOConnect(request, protocols);
			}
  		}, sessionManager);
    }

    /**
     * Returns an instance of SocketIOInbound or null if the connection is to be denied.
     * The value of cookies and protocols may be null.
     */
	protected abstract SocketIOInbound doSocketIOConnect(
			HttpServletRequest request, String[] protocols);
}